﻿using System;
using System.Linq.Expressions;
using System.Reflection;
using K9Nano.Configuration.Startup;
using K9Nano.Dependency;
using K9Nano.Domain;
using K9Nano.Runtime;
using K9Nano.Share.Extensions;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata;

namespace K9Nano.EFCore
{
    public class GlobalFilterProvider : IGlobalFilterProvider, IScopedDependency
    {
        public ISessionContext SessionContext { get; }

        public bool MultiTenancy { get; }

        public Guid? CurrentTenantId  => SessionContext.TenantId;

        public GlobalFilterProvider(ISessionContext sessionContext, IMultiTenancyConfig multiTenancy)
        {
            SessionContext = sessionContext;
            MultiTenancy = multiTenancy.IsEnabled;
        }

        private static readonly MethodInfo ConfigureGlobalFiltersMethodInfo =
            typeof(GlobalFilterProvider).GetMethod(nameof(ConfigureGlobalFilters),
                BindingFlags.Instance | BindingFlags.Public);

        public virtual void ConfigureGlobalFiltersByType(Type entityClrType,
            ModelBuilder modelBuilder, IMutableEntityType mutableEntityType)
        {
            ConfigureGlobalFiltersMethodInfo
                .MakeGenericMethod(entityClrType)
                .Invoke(this, new object[] { modelBuilder, mutableEntityType });
        }

        public virtual void ConfigureGlobalFilters<TEntity>(ModelBuilder modelBuilder, IMutableEntityType entityType)
            where TEntity : class
        {
            if (entityType.BaseType == null && ShouldFilterEntity<TEntity>(entityType))
            {
                var filterExpression = CreateFilterExpression<TEntity>();
                if (filterExpression != null)
                {
                    if (entityType.IsKeyless)
                    {
                        modelBuilder.Entity<TEntity>().HasNoKey().HasQueryFilter(filterExpression);
                    }
                    else
                    {
                        modelBuilder.Entity<TEntity>().HasQueryFilter(filterExpression);
                    }
                }
            }
        }

        protected virtual bool ShouldFilterEntity<TEntity>(IMutableEntityType entityType) where TEntity : class
        {
            if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
            {
                return true;
            }

            if (MultiTenancy)
            {
                if (typeof(IMayHaveTenant).IsAssignableFrom(typeof(TEntity)))
                {
                    return true;
                }

                if (typeof(IMustHaveTenant).IsAssignableFrom(typeof(TEntity)))
                {
                    return true;
                }
            }

            return false;
        }

        protected virtual Expression<Func<TEntity, bool>> CreateFilterExpression<TEntity>()
            where TEntity : class
        {
            Expression<Func<TEntity, bool>> expression = null;

            if (typeof(ISoftDelete).IsAssignableFrom(typeof(TEntity)))
            {
                expression = e => !((ISoftDelete)e).IsDeleted;
            }

            if (typeof(IMayHaveTenant).IsAssignableFrom(typeof(TEntity)))
            {
                Expression<Func<TEntity, bool>> mayHaveTenantFilter =
                    e => ((IMayHaveTenant)e).TenantId == CurrentTenantId;
                expression = expression == null
                    ? mayHaveTenantFilter
                    : CombineExpressions(expression, mayHaveTenantFilter);
            }

            if (typeof(IMustHaveTenant).IsAssignableFrom(typeof(TEntity)))
            {
                Expression<Func<TEntity, bool>> mustHaveTenantFilter =
                    e => ((IMustHaveTenant)e).TenantId == CurrentTenantId;
                expression = expression == null
                    ? mustHaveTenantFilter
                    : CombineExpressions(expression, mustHaveTenantFilter);
            }

            return expression;
        }

        protected virtual Expression<Func<T, bool>> CombineExpressions<T>(Expression<Func<T, bool>> expression1,
            Expression<Func<T, bool>> expression2)
        {
            return ExpressionBuilder.Combine(expression1, expression2);
        }
    }
}